{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Approach"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "CNN with 2 frames"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/sharif/anaconda3/lib/python3.8/site-packages/wandb/util.py:36: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working\n",
      "  from collections import namedtuple, Mapping, Sequence\n",
      "/home/sharif/anaconda3/lib/python3.8/site-packages/wandb/vendor/graphql-core-1.1/graphql/type/directives.py:55: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working\n",
      "  assert isinstance(locations, collections.Iterable), 'Must provide locations for directive.'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "181269041"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import random\n",
    "from collections import OrderedDict\n",
    "from pathlib import Path\n",
    "from PIL import Image\n",
    "from efficientnet_pytorch import EfficientNet\n",
    "\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import Dataset, DataLoader, SubsetRandomSampler, random_split, Subset\n",
    "from torchvision import transforms\n",
    "import pytorch_lightning as pl\n",
    "\n",
    "import wandb\n",
    "from pytorch_lightning.loggers import WandbLogger\n",
    "from pytorch_lightning.callbacks import ModelCheckpoint, EarlyStopping\n",
    "\n",
    "pl.seed_everything(hash(\"setting random seeds\") % 2**32 - 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/sharif/.local/lib/python3.8/site-packages/ipykernel/ipkernel.py:287: DeprecationWarning: `should_run_async` will not call `transform_cell` automatically in the future. Please pass the result to `transformed_cell` argument and any exception that happen during thetransform in `preprocessing_exc_tuple` in IPython 7.17 and above.\n",
      "  and should_run_async(code)\n",
      "\u001b[34m\u001b[1mwandb\u001b[0m: Currently logged in as: \u001b[33msharif\u001b[0m (use `wandb login --relogin` to force relogin)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "wandb.login()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = {\n",
    "    'project_name': 'vehicle-speed-estimation',\n",
    "    'batch_size': 32,\n",
    "    'w': 240, #240 480\n",
    "    'h': 320, #320 640\n",
    "    'model': 'efficientnet-b0',\n",
    "    'split': 0.2,\n",
    "    'mean': .1,\n",
    "    'std': .5,\n",
    "    'divide_y': 10\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "IMAGES = '/home/sharif/Documents/Challenges/commai-challenge/data/frames/train'\n",
    "LABELS = '/home/sharif/Documents/Challenges/commai-challenge/data/labels/train.txt'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfms = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Resize((p['w'],p['h'])),\n",
    "    transforms.Normalize((p['mean'],p['mean'],p['mean']),(p['std'],p['std'],p['std']))\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DS(Dataset):\n",
    "    def __init__(self, images, labels):\n",
    "        self.images = Path(images)\n",
    "        self.labels = open(labels).readlines()\n",
    "        self.n_images = len(list(self.images.glob('*.png')))\n",
    "    def __len__(self): return self.n_images\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        'Returns a random batch of len `seq`'\n",
    "        if idx != 0: idx -= 1\n",
    "        f1 = f'{self.images}/{str(idx)}.png'\n",
    "        f2 = f'{self.images}/{str(idx+1)}.png'\n",
    "        image1 = Image.open(f1)\n",
    "        image2 = Image.open(f2)\n",
    "        img1 = tfms(image1)\n",
    "        img2 = tfms(image2)\n",
    "        x = torch.cat((img1, img2))\n",
    "        y = float(self.labels[idx+1].split()[0])\n",
    "        return x, torch.Tensor([y])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "ds = DS(IMAGES, LABELS)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(16320, 4080)"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_idx = int(len(ds) * (1-p['split']))\n",
    "valid_idx = int(len(ds) * p['split'])\n",
    "train_idx, valid_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_ds = Subset(ds, list(range(train_idx)))\n",
    "valid_ds = Subset(ds, list(range(train_idx, train_idx+valid_idx)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "#train_ds, valid_ds = random_split(ds, [train_idx, valid_idx])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(16320, 4080)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "len(train_ds), len(valid_ds)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y = ds[11]\n",
    "\n",
    "assert len(ds) > 10\n",
    "assert list(y.shape) == [1]\n",
    "assert list(x.shape) == [6,p['w'],p['h']]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(tensor(-0.0451), tensor(0.2354))"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x.mean(), x.std()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Model(pl.LightningModule):\n",
    "    def __init__(self, p):\n",
    "        super().__init__()\n",
    "        self.hparams = p\n",
    "        self.save_hyperparameters()\n",
    "        \n",
    "        self.en = EfficientNet.from_pretrained(p['model'], in_channels=6, num_classes=1)\n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.en(x)\n",
    "    \n",
    "    def step(self, batch, batch_idx):\n",
    "        x,y = batch\n",
    "        y /= p['divide_y']\n",
    "        y_hat = self(x)\n",
    "        loss = F.mse_loss(y_hat, y)\n",
    "        abs_loss = torch.abs(y_hat-y).mean().sum()\n",
    "        return OrderedDict({\n",
    "            'loss': loss,\n",
    "            'abs_loss': abs_loss\n",
    "        })\n",
    "        \n",
    "    def training_step(self, batch, batch_idx):\n",
    "        out = self.step(batch, batch_idx)\n",
    "        self.log('train_batch_loss', out['loss'])\n",
    "        self.log('train_batch_abs_loss', out['abs_loss'])\n",
    "        return out\n",
    "    \n",
    "    def training_epoch_end(self, outputs):\n",
    "        loss = torch.stack([output['loss'] for output in outputs]).float().mean()\n",
    "        abs_loss = torch.stack([output['abs_loss'] for output in outputs]).float().mean()\n",
    "        self.log('train_loss', loss)\n",
    "        self.log('train_abs_loss', abs_loss)\n",
    "    \n",
    "    def validation_step(self, batch, batch_idx):\n",
    "        out = self.step(batch, batch_idx)\n",
    "        self.log('val_batch_loss', out['loss'])\n",
    "        self.log('val_batch_abs_loss', out['abs_loss'])\n",
    "        return out\n",
    "    \n",
    "    def validation_epoch_end(self, outputs):\n",
    "        loss = torch.stack([output['loss'] for output in outputs]).float().mean()\n",
    "        abs_loss = torch.stack([output['abs_loss'] for output in outputs]).float().mean()\n",
    "        self.log('val_abs_loss', abs_loss)\n",
    "        self.log('val_loss', loss)\n",
    "    \n",
    "    def configure_optimizers(self): return optim.Adam(self.parameters())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_dl = DataLoader(train_ds, shuffle=True, batch_size=p['batch_size'], num_workers=6, pin_memory=True)\n",
    "valid_dl = DataLoader(valid_ds, shuffle=True, batch_size=p['batch_size'], num_workers=6, pin_memory=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "wandb_logger = WandbLogger(project=\"vehicle-speed-estimation\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "checkpoint = ModelCheckpoint(\n",
    "    monitor='val_loss',\n",
    "    filename='cnn-{epoch:02d}-{val_loss:.4f}',\n",
    "    save_top_k=2,\n",
    "    mode='min'\n",
    ")\n",
    "\n",
    "early_stopping = EarlyStopping(\n",
    "    monitor='val_loss',\n",
    "    patience=5,\n",
    "    verbose=True,\n",
    "    mode='min'\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "GPU available: True, used: True\n",
      "TPU available: False, using: 0 TPU cores\n",
      "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n"
     ]
    }
   ],
   "source": [
    "trainer = pl.Trainer(gpus=1,\n",
    "                     fast_dev_run=False,\n",
    "                     log_every_n_steps=10,\n",
    "                     logger=wandb_logger,\n",
    "                     #overfit_batches=5,\n",
    "                     deterministic=True,\n",
    "                     callbacks=[checkpoint,early_stopping]\n",
    "                    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Loaded pretrained weights for efficientnet-b0\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "\n",
       "                Tracking run with wandb version 0.10.10<br/>\n",
       "                Syncing run <strong style=\"color:#cdcd00\">vivid-voice-19</strong> to <a href=\"https://wandb.ai\" target=\"_blank\">Weights & Biases</a> <a href=\"https://docs.wandb.com/integrations/jupyter.html\" target=\"_blank\">(Documentation)</a>.<br/>\n",
       "                Project page: <a href=\"https://wandb.ai/sharif/vehicle-speed-estimation\" target=\"_blank\">https://wandb.ai/sharif/vehicle-speed-estimation</a><br/>\n",
       "                Run page: <a href=\"https://wandb.ai/sharif/vehicle-speed-estimation/runs/1hfpw7lx\" target=\"_blank\">https://wandb.ai/sharif/vehicle-speed-estimation/runs/1hfpw7lx</a><br/>\n",
       "                Run data is saved locally in <code>/home/sharif/Documents/Challenges/vehicle-speed-estimation-v2/wandb/run-20201116_183806-1hfpw7lx</code><br/><br/>\n",
       "            "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n",
      "  | Name | Type         | Params\n",
      "--------------------------------------\n",
      "0 | en   | EfficientNet | 4 M   \n",
      "/home/sharif/anaconda3/lib/python3.8/site-packages/pytorch_lightning/utilities/distributed.py:45: UserWarning: Your val_dataloader has `shuffle=True`, it is best practice to turn this off for validation and test dataloaders.\n",
      "  warnings.warn(*args, **kwargs)\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validation sanity check'), FloatProgress(value=1.0, bar_style='info', layout=Layout…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "b47e942bfcbe45c8a1d585957715daec",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Training'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), max…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(HTML(value='Validating'), FloatProgress(value=1.0, bar_style='info', layout=Layout(flex='2'), m…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "m = Model(p)\n",
    "trainer.fit(m, train_dl, valid_dl)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<br/>Waiting for W&B process to finish, PID 33811<br/>Program ended successfully."
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "wandb.finish()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#ckp = '/home/sharif/Documents/Challenges/vehicle-speed-estimation-v2/vehicle-speed-estimation/22fcqxam/checkpoints/cnn-epoch=02-val_loss=0.8512.ckpt'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = Model(p)\n",
    "m = m.load_from_checkpoint(ckp);\n",
    "m.eval();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x,y = valid_ds[190]\n",
    "print(y)\n",
    "x.shape\n",
    "y_hat = m(x.unsqueeze(0))\n",
    "print(y_hat)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "1/32*(y_hat - y)**2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def inference(f1, f2):\n",
    "    image1 = Image.open(f1)\n",
    "    image2 = Image.open(f2)\n",
    "    img1 = tfms(image1)\n",
    "    img2 = tfms(image2)\n",
    "    x = torch.cat((img1, img2)).unsqueeze(0)\n",
    "    return m(x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f1 = '/home/sharif/Documents/Challenges/commai-challenge/data/frames/test/3000.png'\n",
    "f2 = '/home/sharif/Documents/Challenges/commai-challenge/data/frames/test/3001.png'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "inference(f1,f2)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
